<?php

/**
 * Module bootstrap base class
 *
 * A module bootstrap executes any additional functions that are needed by the
 * module actions to work properly.
 *
 * A module bootstrap is also made available to any controller via the
 * Mz_Module_Helper_Module action helper (registered automatically by the
 * Mz_Module_Initializer plugin), so subclasses of this abstract can also
 * contain methods that relate to the module which you might want to invoke
 * elsewhere. E.g., a Blog module bootstrap might contain a method that
 * returns a list of recent blog entries.
 *
 * @author    Mon Zafra <monzee at gmail>
 * @copyright (c)2009 Mon Zafra
 * @package   Mz
 * @category  Module
 * @license   http://monzee.wordpress.com/license New BSD
 * @version   $Id: Bootstrap.php 38 2009-02-18 17:12:39Z monzee $
 */
abstract class Mz_Module_Bootstrap
{
    /**
     * Prefix of module-specific helpers and other classes.
     * Typically the module name itself
     *
     * @var string
     */
    static protected $_prefix;

    /**
     * Module path
     *
     * @var string
     */
    static protected $_path;

    /**
     * Spec of the generated links to internal module resources
     *
     * @var string
     */
    static protected $_resourceUrlSpec = ':base/:resmodule/:prefix/:resource';

    /**
     * Has the preRoute() method been called already?
     *
     * @var bool
     */
    static protected $_preRouted = false;

    /**
     * Front controller
     *
     * @var Zend_Controller_Front
     */
    protected $_front;

    /**
     * Register autoloader on instantiation
     *
     * @param string $module Module name
     * @param string $path   Module path
     */
    public function __construct($module, $path)
    {
        $module = ucfirst(strtolower($module));
        self::$_prefix = rtrim($module, '_');

        self::$_path = $path;

        spl_autoload_register(array($this, 'load'));

        if (!self::$_preRouted) {
            $this->preRoute($module, $path);
        }
        $this->init();
        
        // because you might need to instantiate it again
        self::$_preRouted = false;
    }

    /**
     * Get front controller instance
     * 
     * @return Zend_Controller_Front
     */
    public function getFrontController()
    {
        if (null === $this->_front) {
            $this->_front = Zend_Controller_Front::getInstance();
        }

        return $this->_front;
    }

    /**
     * Register plugin if it hasn't been registered with the front controller
     *
     * @param string $pluginName Plugin class name
     * @param int    $stack      stack index
     * @return Mz_Module_Bootstrap
     */
    protected function _registerIfNone($pluginName, $stack = null)
    {
        $front = $this->getFrontController();
        if (!$front->hasPlugin($pluginName)) {
            $plugin = new $pluginName();
            $front->registerPlugin($plugin, $stack);
        }

        return $this;
    }

    /**
     * Aggressive module initialization
     * 
     * This method is called (if registered) upon the instantiation of the module
     * initializer regardless of whether or not the request maps to this module.
     * This is essentially an extension of the application bootstrap.
     * 
     * This hook would typically contain functions that you want to execute
     * regardless of the module dispatched. For example, if your module needs to
     * have a certain routing rule set to function properly, they should be put 
     * in preRoute() and not in init(). If the routes were set in init(), it
     * would have been too late to take effect.
     *
     * Any module which implements this must be registered to the module
     * initializer via the constructor option (preRouteHook) or
     * Mz_Module_Initializer::addPreRouteHook(). For the latter, make sure that
     * the addPreRouteHook() is called _before_ the module initializer plugin is
     * instantiated.
     *
     * This method is always called if the request maps to any controller in
     * this module.
     *
     * @param string $module Module name
     * @param string $path   Module path
     */
    static public function preRoute($module, $path) { }

    /**
     * Lazy module initialization
     *
     * This is also essentially an extension of the application bootstrap but
     * unlike preRoute(), this method is only executed if the request maps to
     * this module, or if a module bootstrap method is specifically invoked
     * in a controller action via $this->_helper->module($module).
     * 
     * Register plugins and helpers needed by the module controllers here and/or
     * add internal module paths to the include path. This is unconditionally
     * executed right after instantiation.
     */
    public function init() { }

    /**
     * Prefix of module-specific classes.
     *
     * @param string $prefix
     */
    static public function setPrefix($prefix)
    {
        self::$_prefix = $prefix;
    }

    /**
     * Set module path. Not sure if this would ever be used
     *
     * @param string $path
     */
    static public function setPath($path)
    {
        self::$_path = $path;
    }

    /**
     * Set resource url pattern
     *
     * @param string $spec 
     */
    static public function setResourceUrlSpec($spec)
    {
        self::$_resourceUrlSpec = $spec;
    }

    /**
     * Set the subpath in the public directory where this module's resources
     * map to
     * 
     * @param string $path 
     */
    static public function setPublicSubpath($path)
    {
        $path = trim($path, '/');
        self::$_resourceUrlSpec = ':base/' . $path . '/:resource';
    }

    /**
     * Set prerouted state
     * 
     * @param bool $state 
     */
    static public function setPreRouted($state = true)
    {
        self::$_preRouted = $state;
    }

    /**
     * Load files inside the module directory.
     *
     * Registered as an autoloader automatically when instantiated. This method
     * should map classes which cannot be loaded by the main app to files inside
     * this module directory.
     *
     * @param string $class Class name
     * @return bool Was the class file found?
     */
    static public function load($class)
    {
        $prefix = self::$_prefix;

        if ($prefix == substr($class, 0, strlen($prefix))) {
            $unprefixed = substr($class, strlen($prefix));
            $trans = str_replace('_', DIRECTORY_SEPARATOR, $unprefixed) . '.php';

            $file = self::pathTo('resources' . DIRECTORY_SEPARATOR . $trans);
            if (file_exists($file)) {
                include_once $file;
                return true;
            }

            $file = self::pathTo('models' . DIRECTORY_SEPARATOR . $trans);
            if (file_exists($file)) {
                include_once $file;
                return true;
            }
        }

        $trans = str_replace('_', DIRECTORY_SEPARATOR, $class) . '.php';
        $file = self::pathTo('library' . DIRECTORY_SEPARATOR . $trans);
        if (file_exists($file)) {
            include_once $file;
            return true;
        }

        return false;
    }

    /**
     * Return path relative to module base dir
     *
     * This shouldn't be used in the preRoute() method because the $_path property
     * would still be blank by then. Late static binding in 5.3 would make it
     * possible to autodiscover the module path.
     *
     * @param string $path String to append to base module path
     * @return string
     */
    static public function pathTo($path = null)
    {
        $ret = self::$_path;

        if (!empty($path)) {
            $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, trim($path, '/\\'));
            $ret .= DIRECTORY_SEPARATOR . $path;
        }

        return $ret;
    }

    /**
     * Generates a link to an internal module resource
     * 
     * Recommended project structure: ('foo' is the module name)
     *
     * project
     * + application
     * | +-modules
     * |   +-foo
     * |     +-public
     * |       +-resource.ext
     * +-public
     *   +-resource
     *     +-foo
     *       +-resource.ext
     *
     * The resource mapper plugin/module can automatically fetch any file
     * in project/application/modules/foo/public when invoked with the link
     * http://host/<base_uri>/resource/foo/<filename>. If there already exists a
     * module by the name of resource, _resource is used as the module name.
     * 
     * @param string $resource
     * @param string $baseUrl
     * @param string $prefix
     * @return string
     * @todo Autofill $baseUrl if null?
     */
    static public function urlToResource($resource = null, $baseUrl = null, $prefix = null)
    {
        $spec = self::$_resourceUrlSpec;
        $prefix = null === $prefix ? strtolower(self::$_prefix) : $prefix;
        $modRes = Zend_Controller_Front::getInstance()->getPlugin('Mz_Module_Resource');

        $replaceMap = array(
            ':resmodule' => $modRes->getResourcesModule(),
            ':base' => $baseUrl,
            ':prefix' => $prefix,
            ':resource' => $resource,
        );

        return str_replace(array_keys($replaceMap), array_values($replaceMap), $spec);
    }

}
